using System;
using System.Collections.Generic;
using System.Text;
using System.Runtime.InteropServices;
using System.Threading;

using DirectShowLib;

namespace Pg.BioMedics.RatObserver.Media
{
    /// <summary>
    /// PlayControl controler class - created for specified loaded 
    /// Video object. Instance of this object allows video playback
    /// control by simple methods invocation.
    /// </summary>
    public class PlayControl : IDisposable
    {
        #region Fields

        private readonly IGraphBuilder _GraphBuilder;
        private readonly IMediaControl _MediaControl;
        private readonly IMediaEvent _MediaEvent;
        private readonly IMediaSeeking _MediaSeeking;

        private readonly ManualResetEvent _GraphEventNotifier;
        private PlaybackState _State;
        
        #endregion

        #region Constructors

        /// <summary>
        /// Initializes instance of playback controller class with
        /// DirectShow graph play control and play event objects.
        /// <param name="graphBuilder">DirectShow graph builder</param>
        /// </summary>
        internal PlayControl(IGraphBuilder graphBuilder)
        {
            // input params validation
            if (graphBuilder == null)
                throw new ArgumentNullException("mediaControl", "Can't initialize play control object, graph builder object can't be null");

            try
            {
                // initialize DShow interfaces
                _GraphBuilder = graphBuilder;
                _MediaControl = graphBuilder as IMediaControl;
                _MediaEvent = graphBuilder as IMediaEvent;
                _MediaSeeking = graphBuilder as IMediaSeeking;

                // initialize graph watcher
                IntPtr eventHandle;
                int hr = _MediaEvent.GetEventHandle(out eventHandle);
                DsError.ThrowExceptionForHR(hr);

                _GraphEventNotifier = new ManualResetEvent(false);
                _GraphEventNotifier.SafeWaitHandle = new Microsoft.Win32.SafeHandles.SafeWaitHandle(eventHandle, true);

                Thread eventWatchThread = new Thread(new ThreadStart(WatchGraphEvents));
                eventWatchThread.Start();
            }
            catch (Exception ex)
            {
                throw new MediaException("Can't initialize play control object", ex);
            }
        }

        /// <summary>
        /// Deinitializes play control class
        /// </summary>
        ~PlayControl()
        {
            Dispose();
        }

        #endregion

        #region Properties

        /// <summary>
        /// Current playback progress
        /// </summary>
        public int Progress
        {
            get
            {
                try
                {
                    long stopTime = 0;
                    long currentTime = 0;

                    int hr = _MediaSeeking.GetPositions(out currentTime, out stopTime);
                    DsError.ThrowExceptionForHR(hr);

                    return (int)((currentTime * 100) / stopTime); // percentage progress
                }
                catch (Exception ex)
                {
                    throw new MediaException("Can't retrive playback progress", ex);
                }
            }
        }

        /// <summary>
        /// Current playback state
        /// </summary>
        public PlaybackState State
        {
            get
            {
                return _State;
            }
        }

        #endregion

        #region Methods

        /// <summary>
        /// Starts playback execution or resumes 
        /// if suspended
        /// </summary>
        public void Play()
        {
            try
            {
                // play
                int hr = _MediaControl.Run();
                DsError.ThrowExceptionForHR(hr);

                // change state
                _State = PlaybackState.Running;

                // invoke events
                if (OnPlay != null)
                    OnPlay.Invoke(this, EventArgs.Empty);
            }
            catch (Exception ex)
            {
                throw new MediaException("Can't start video playback", ex);
            }
        }

        /// <summary>
        /// Freezes on current frame
        /// </summary>
        public void Pause()
        {
            try
            {
                // pause
                int hr = _MediaControl.Pause();
                DsError.ThrowExceptionForHR(hr);

                // change state
                _State = PlaybackState.Paused;

                // invoke events
                if (OnPause != null)
                    OnPause.Invoke(this, EventArgs.Empty);
            }
            catch (Exception ex)
            {
                throw new MediaException("Can't pause video playback", ex);
            }
        }

        /// <summary>
        /// Stops playback and moves current frame pointer to 
        /// the beginning
        /// </summary>
        public void Stop()
        {
            try
            {
                // stop playback
                int hr = _MediaControl.Stop();
                DsError.ThrowExceptionForHR(hr);

                // move position to the beginning
                hr = _MediaSeeking.SetPositions(0, AMSeekingSeekingFlags.AbsolutePositioning,
                                                null, AMSeekingSeekingFlags.NoPositioning);
                DsError.ThrowExceptionForHR(hr);

                // display first frame
                hr = _MediaControl.Pause();
                DsError.ThrowExceptionForHR(hr);

                // change state
                _State = PlaybackState.Stopped;

                // invoke event
                if (OnStop != null)
                    OnStop.Invoke(this, EventArgs.Empty);
            }
            catch (Exception ex)
            {
                throw new MediaException("Can't stop video playback", ex);
            }
        }

        /// <summary>
        /// Rewindes currently played video playback.
        /// If current playback state is stopped, it won't have 
        /// inpact on playback progress.
        /// If rewind parameter is greater than available playback period
        /// current position will be moved to the end or the begining of the movie (with respect if positive or negative value was provided)
        /// </summary>
        /// <param name="percent">rewind progress change (100 - skips from the beginning to the end, 0 - does nothing)</param>
        public void Rewind(int percent)
        {
            try
            {
                long stop = 0;
                long current = 0;
                long next = 0;

                // get current and stop positions
                int hr = _MediaSeeking.GetPositions(out current, out stop);
                DsError.ThrowExceptionForHR(hr);

                // calculate next new frame pointer
                next = current + (long)((double)stop * (double)percent / 100.0d);
                next = next > stop ? stop : next;
                next = next < 0 ? 0 : next;

                // seek
                hr = _MediaSeeking.SetPositions(next , AMSeekingSeekingFlags.AbsolutePositioning, 
                                                stop, AMSeekingSeekingFlags.AbsolutePositioning);
                DsError.ThrowExceptionForHR(hr);

                // invoke events
                if (OnRewind != null)
                    OnRewind.Invoke(this, EventArgs.Empty);
            }
            catch (Exception ex)
            {
                throw new MediaException("Can't rewind video playback", ex);
            }
        }

        #endregion

        #region Support methods

        /// <summary>
        /// Private support method utilized as a graph event watcher
        /// </summary>
        private void WatchGraphEvents()
        {
            do
            {
                // wait for event
                _GraphEventNotifier.WaitOne(-1, true);

                lock (this)
                {
                    if (_State == PlaybackState.Running)
                    {
                        // get event
                        EventCode eventCode;
                        int lParam1, lParam2;
                        for (int hr = _MediaEvent.GetEvent(out eventCode, out lParam1, out lParam2, 0); hr >= 0;
                                 hr = _MediaEvent.GetEvent(out eventCode, out lParam1, out lParam2, 0))
                        {
                            // check event
                            if (eventCode == EventCode.Complete) // finished
                                Stop();

                            // free event params
                            hr = _MediaEvent.FreeEventParams(eventCode, lParam1, lParam2);
                            DsError.ThrowExceptionForHR(hr);
                        }
                    }
                    else if (_State == PlaybackState.Exiting) // unload graph watch thread
                        return;
                }
            }
            while (true);
        }

        #endregion

        #region Events

        public EventHandler OnPlay;
        public EventHandler OnPause;
        public EventHandler OnStop;
        public EventHandler OnRewind;

        #endregion

        #region IDisposable Members

        public void Dispose()
        {
            // set state
            _State = PlaybackState.Exiting;

            // release DShow objects
            if (_MediaControl != null)
                Marshal.ReleaseComObject(_MediaControl);

            if (_MediaEvent != null)
                Marshal.ReleaseComObject(_MediaEvent);

            if (_MediaSeeking != null)
                Marshal.ReleaseComObject(_MediaSeeking);

            if (_GraphBuilder != null)
                Marshal.ReleaseComObject(_GraphBuilder);

            // run GC
            GC.Collect();
            GC.WaitForPendingFinalizers();
        }

        #endregion
    }

    public enum PlaybackState
    {
        Stopped,
        Paused,
        Running,
        Exiting
    }
}
